package com.example.spider.task;

import com.example.spider.proxy.core.ProxyInfo;
import com.example.spider.task.limit.LimitStrategy;
import com.example.spider.util.HeaderUtil;
import com.example.spider.util.HtmlUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import org.springframework.core.GenericTypeResolver;
import org.springframework.http.HttpMethod;

import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

/**
 * common 任务
 *
 * @author lym
 */
@Getter
@Setter
@Accessors(chain = true)
public class AbstractSpiderTask<T> implements Task<T> {

    //protected String realClassTypeForJsonSerial;

    /**
     * 域名
     */
    protected String domain;

    protected String url;

    protected Object[] params;

    protected String requestMethod;

    protected Map<String, String> headers;

    protected Map<String, String> responseHeaders;

    protected Charset charset;

    /**
     * 请求失败后重试次数
     */
    protected int retryNum;

    protected transient Task<?> parent;

    protected ProxyInfo exceptedProxy;

    protected ProxyInfo actuallyProxy;

    protected Class<T> resultType = resolveResultType();

    /**
     * 请求耗时ms
     */
    private long requestCost;

    /**
     * 本地幂等文件路径
     */
    private String idempotentFilePath;

    /**
     * 本地幂等文件路径
     */
    private Function<AbstractSpiderTask<? extends T>, T> idempotentFunction;

    /**
     * 如何限流
     */
    protected LimitStrategy limitStrategy;


    public AbstractSpiderTask() {
        requestMethod = HttpMethod.GET.name();
        headers = HeaderUtil.defaultHeaders();
        // todo subtype
        //JsonUtils.createObjectMapper().registerSubtypes(
        //        new NamedType(getClass(), getClass().getSimpleName())
        //);
    }

    @SuppressWarnings("unchecked")
    public <X extends AbstractSpiderTask<T>> X setUrl(String url) {
        int splitIndex = url.indexOf("#");
        // 去掉定位符号后的内容
        this.url = splitIndex > 0 ? url.substring(0, splitIndex) : url;
        this.domain = HtmlUtil.parseDomainFromUrl(url);
        this.headers = HeaderUtil.defaultHeaders(url);
        return (X) this;
    }

    @SuppressWarnings("unchecked")
    public <X extends AbstractSpiderTask<T>> X setApiUrl(String url) {
        this.url = url;
        this.domain = HtmlUtil.parseDomainFromUrl(url);
        this.headers = HeaderUtil.defaultHeaders(url);
        return (X) this;
    }

    @SuppressWarnings("unchecked")
    public <X extends AbstractSpiderTask<T>> X addHeader(String key, String value) {
        this.headers.put(key, value);
        return (X) this;
    }

    public <X extends AbstractSpiderTask<T>> X setIdempotentFilePath(String path) {
        this.idempotentFilePath = path;
        return (X) this;
    }

    public <X extends AbstractSpiderTask<T>> X setIdempotentFunction(Function<AbstractSpiderTask<? extends T>, T> idempotentFunction) {
        this.idempotentFunction = idempotentFunction;
        return (X) this;
    }


    public boolean canRetry() {
        // 失败次数太多没必要再试了，一般小的请求没必要重试太多
        return retryNum < 10;
    }

    @SuppressWarnings("unchecked")
    protected Class<T> resolveResultType() {
        return (Class<T>) Optional.ofNullable(GenericTypeResolver.resolveTypeArguments(getClass(), AbstractSpiderTask.class))
                // null 表示反序列化创建 object
                .map(arr -> arr[0]).orElse(null);
    }

    public T calculateIdempotentResult() {
        return Optional.ofNullable(idempotentFunction).map(f -> f.apply(this))
                .orElse(getIdempotentPathOptional()
                        .map(p -> useLocalCache(this, p))
                        .orElse(null));
    }

    public Optional<Path> getIdempotentPathOptional() {
        return Optional.ofNullable(getIdempotentFilePath())
                .map(Path::of)
                .filter(Files::exists);
    }

    @SneakyThrows
    private static <T> T useLocalCache(Task<T> task, Path localPath) {
        if (task.getResultType() == byte[].class) {
            return (T) Files.readAllBytes(localPath);
        }
        return (T) Files.readString(localPath);
    }

}
